<?php
/**
 * Copyright (c) 2006, PORTALIER Julien
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @package      FeatherPHP
 * @subpackage   Model
 * @copyright    Copyright (c) 2006, Julien PORTALIER
 * @link         http://feather.portalier.com
 * @license      http://www.opensource.org/licenses/mit-license.php The MIT License
 */

abstract class Model
{
	public $name;
	public $dbname;
	
	// CRUD
	protected $belongsTo           = array();
	protected $hasMany             = array();
	protected $hasOne              = array();
	protected $hasOneToMany        = array();
	protected $hasAndBelongsToMany = array();
	
	protected $_belongsTo;
	protected $_hasMany;
	protected $_hasOne;
	
	// DATA
	public    $id     = null;
	public    $data   = array();
	public    $validationErrors = array();
	protected $validate;
	
	// APP
	protected $controller;
	public    $params;
	public    $outdated = null;
	
	// DB
	public    $db;
	protected $dbConfig = 'default';
	protected $useTable;
	public    $table;
	public    $fields;
	public    $primaryKey = 'id';
	public    $foreignKey;
	
	// queries
	public    $defaultJoin      = 'LEFT';
	protected $defaultProcess   = Q_PROCESS_FULL;
	public    $process          = Q_PROCESS_FULL;
	public    $reset            = true;
	
	protected $defaultRecursive = 1;
	public    $_recursive;
	public    $recursive;
	public    $recursiveProcess = Q_PROCESS_LITE;
	public    $recursiveFields  = '*';
	
	public    $defaultFields   = '*';
	public    $defaultOrder    = '';
	protected $defaultOperator = 'AND';
	
	public    $_join;
	
	protected $_joins;
	protected $_operator;
	protected $_conditions  = array();
	protected $_order       = '';
	protected $_groupby     = '';
	protected $_limit       = '';
	
	function __construct(Controller $controller=null)
	{
		$this->name   = get_class($this);
		$this->load   = Loader::instance('Loader');
		
		if ($this->useTable !== false)
		{
			$this->db     = Database::get($this->dbConfig);
			
			// table
			if (empty($this->useTable))
			{
				$this->table      = Inflector::tableize($this->name);
				$this->foreignKey = Inflector::underscore($this->name).'_'.$this->primaryKey;
			}
			else
			{
				$this->table      = $this->useTable;
				$this->foreignKey = $this->useTable.'_'.$this->primaryKey;
			}
			
			// fields
			$this->fields = cache('models'.DS.$this->table);
			
			if (!empty($this->fields))
				$this->fields = unserialize($this->fields);
			else
			{
				$this->fields = $this->db->listFields($this->table);
				cache('models'.DS.$this->table, serialize($this->fields));
			}
			
			$this->table  = $this->db->field($this->table);
			$this->dbname = $this->db->field($this->name);
		}
		
		// data
		if (!is_null($controller))
		{
			$this->controller = $controller;
			$this->params =& $controller->params;
			$this->data   =& $controller->data;
		}
		
		// associations
		if (!empty($this->hasOneToMany))
		{
			$this->hasOne  = array_merge($this->hasOne,  $this->hasOneToMany);
			$this->hasMany = array_merge($this->hasMany, $this->hasOneToMany);
		}
		if (!empty($this->hasAndBelongsToMany))
		{
			$this->belongsTo = array_merge($this->belongsTo, $this->hasAndBelongsToMany);
			$this->hasMany   = array_merge($this->hasMany,   $this->hasAndBelongsToMany);
		}
		
		// defaults
		$this->recursive  = $this->defaultRecursive;
		$this->process    = $this->defaultProcess;
		$this->_join      = $this->defaultJoin;
		$this->_operator  = $this->defaultOperator;
		
		$this->_belongsTo = $this->belongsTo;
		$this->_hasMany   = $this->hasMany;
		$this->_hasOne    = $this->hasOne;
	}
	
	protected function __get($model)
	{
		$this->$model = $this->load->model($model);
		return $this->$model;
	}
	
	protected function & __call($func, $args)
	{
		preg_match('/(find(|All))By([\w\d]+)/', $func, &$match);
		if (!empty($match))
		{
			$this->conditions(array($this->name.'.'.strtolower($match[3]) => array_shift($args)));
			$rs = call_user_func_array(array($this, $match[1]), $args);
			return $rs;
		}
		trigger_error(s_('Unknown method %s->%s', $this->name, $func));
		die();
	}
	
	
	// dynamic associations
	
	function bind($assocs)
	{
		foreach($assocs as $type => $models)
		{
			if (!is_array($models))
				$models = explode(',', $models);
			$this->$type      = array_merge($this->$type, $models);
			$this->{"_$type"} = array_merge($this->{"_$type"}, $models);
		}
		$this->_modifiedModels = true;
	}
	
	function unbind($assocs, $permanent=false, $_unset=false)
	{
		foreach($assocs as $type => $models)
		{
			if (!is_array($models))
				$models = explode(',', $models);
			foreach ($models as $model)
			{
				if ($permanent)
				{
					$key = array_search($model, $this->$type);
					if ($key !== false)
						unset($this->{$type}[$key]);
				}
				$key = array_search($model, $this->{"_$type"});
				if ($key !== false)
					unset($this->{"_$type"}[$key]);
			}
		}
		$this->_modifiedModels = true;
	}
	
	
	// queries
	
	function conditions($conditions)
	{
		$this->_conditions = array_merge($this->_conditions, $conditions);
	}
	function operator($operator) { $this->_operator = $operator;                    }
	function fields($fields)     { $this->_fields   = $this->db->fields($fields);   }
	function order($order)       { $this->_order    = $this->db->order($order);     }
	function groupby($groupby)   { $this->_groupby  = $this->db->groupby($groupby); }
	function limit($limit)       { $this->_limit    = $this->db->limit($limit);     }
	
	function join($leftClass, $rightClass)
	{
		$this->_joins .= " {$this->_join} JOIN {$leftClass->table} AS ".$this->db->field($leftClass->name)." ON ".
			$this->db->field("{$leftClass->name}.{$rightClass->primaryKey}")." = ".
			$this->db->field("{$rightClass->name}.{$leftClass->foreignKey}");
	}
	
	function & findAll($fields=null, $limit=null, $page=null)
	{
		// joins
		$joins = '';
		if ($this->recursive > -1)
		{
			foreach($this->_belongsTo as $j)
			{
				$joins .= " {$this->_join} JOIN {$this->$j->table} AS ".$this->db->field($j)." ON ".
					$this->db->field("$j.{$this->$j->primaryKey}")." = ".
					$this->db->field("{$this->name}.{$this->$j->foreignKey}");
			}
			foreach($this->_hasOne as $j)
			{
				$joins .= " {$this->_join} JOIN {$this->$j->table} AS ".$this->db->field($j)." ON ".
					$this->db->field("$j.{$this->foreignKey}")." = ".
					$this->db->field("{$this->name}.{$this->primaryKey}");
			}
		}
		$joins .= $this->_joins;
		
		// limit
		if (isset($limit) or isset($page))
			$this->_limit = $this->db->limit($limit, $page);
		
		// fields
		if (empty($fields))
			$fields = empty($this->_fields) ? $this->defaultFields : $this->_fields;
		$fields = $this->db->fields($fields);
		
		// query
		$conditions = $this->db->conditions($this->_conditions, $this->_operator);
		$sql        = "SELECT $fields FROM {$this->table} AS {$this->dbname} $joins " .
		              "$conditions {$this->_groupby} {$this->_order} {$this->_limit} ;";
		$results    = $this->db->query($sql, $this->process);
		
		// recursive
		if ($this->recursive > 0 and $this->process == Q_PROCESS_FULL and !empty($this->_hasMany) and !empty($results))
		{
			foreach($this->_hasMany as $a)
			{
				// prepares
				if (!empty($this->$a->defaultOrder))
					$this->$a->order($this->$a->defaultOrder);
				$this->$a->recursive = isset($this->$a->_recursive) ? $this->$a->_recursive : -1;
				$this->$a->process   = $this->$a->recursiveProcess;
				$this->$a->reset     = false;
				
				// queries
				foreach($results as $index => $row)
				{
					$this->$a->conditions(array("$a.{$this->foreignKey}" => $row[$this->name][$this->primaryKey]));
					$results[$index][$a] = $this->$a->findAll($this->$a->recursiveFields);
				}
				
				// cleans up
				$this->$a->__reset();
				$this->$a->reset = true;
			}
		}
		
		if ($this->reset)
			$this->__reset();
		
		return $results;
	}
	
	function __reset()
	{
		if (isset($this->_modifiedModels))
		{
			$this->_hasMany   = $this->hasMany;
			$this->_hasOne    = $this->hasOne;
			$this->_belongsTo = $this->belongsTo;
			$this->_modifiedModels = null;
		}
		$this->recursive   = $this->defaultRecursive;
		$this->_operator   = $this->defaultOperator;
		$this->_join       = $this->defaultJoin;
		$this->_joins      = '';
		$this->_order      = '';
		$this->_groupby    = '';
		$this->_limit      = '';
		$this->_conditions = array();
		$this->process     = $this->defaultProcess;
	}
	
	function & find($fields='*')
	{
		$rs = $this->findAll($fields, 1);
		$rs = empty($rs) ? false : $rs[0];
		return $rs;
	}
	
	function & read($fields='*', $id=null)
	{
		if (empty($this->_conditions) and (isset($id) or isset($this->id)))
			$this->conditions(array("{$this->name}.id" => is_null($id) ? $this->id : $id));
		$rs = $this->find($fields);
		return $rs;
	}
	
	function field($field, $id=null, $recursive=0)
	{
		$this->process   = Q_PROCESS_NONE;
		$this->recursive = $recursive;
		$rs = $this->read($field, $id);
		return empty($rs) ? false : $rs[0];
	}
	
	function findCount($recursive=0, $reset=true, $field=null)
	{
		$_recursive = $this->recursive; $this->recursive = $recursive;
		$_reset     = $this->reset;     $this->reset     = $reset;
		$_process   = $this->process;   $this->process   = Q_PROCESS_NONE;
		
#		$rs = $this->find(empty($field) ? "-! COUNT(*)" : "-! DISTINCT COUNT(".$this->db->field($field).")");
		$field = empty($field) ? '*' : $this->db->field("DISTINCT $field");
		$rs = $this->find("-! COUNT($field)");
		
		$this->reset     = $_reset;
		$this->process   = $_process;
		$this->recursive = $_recursive;
		return empty($rs) ? false : $rs[0];
	}
	
	/**
	 * @todo Model::findNeighbours is just ugly coded.
	 */
	function & findNeighbours($field, $value, $fields=null, $prev=true, $next=true)
	{
		$this->reset = false;
		$rs = array();
		
		$conditions = $this->_conditions;
		
		if ($prev)
		{
			$this->order("$field DESC");
			$this->_conditions[$field][] = "< $value";
			$rs['prev'] = $this->find($fields);
		}
		
		if ($next)
		{
			$this->order("$field ASC");
			$this->_conditions = $conditions;
			$this->_conditions[$field][] = "> $value";
			
			$rs['next'] = $this->find($fields);
		}
		
		$this->__reset();
		return $rs;
	}
	
	function & listField($field, $recursive=null)
	{
		if (!is_null($recursive))
			$this->recursive = $recursive;
		$this->process = Q_PROCESS_LIST;
		
		$rs = $this->findAll("DISTINCT $field");
		return $rs;
	}
	
	
	// validations
	
	function validates($data=null)
	{
		if (!is_null($data))
			$this->__setData(&$data);
		
		require_once APP.'config'.DS.'validations.php';
		
		$valid = $this->beforeValidate();
		
		// self
		$valid = $this->__validates();
		
		// related
		if ($this->recursive)
		{
			foreach ($this->__getRelated() as $model)
			{
				if (isset($this->data[$model]))
				{
					$valid &= $this->$model->validates();
					if (!empty($this->$model->validationErrors))
					{
						$this->validationErrors = array_merge($this->validationErrors,
							$this->$model->validationErrors);
					}
				}
			}
		}
		
		return ($this->afterValidate() && $valid);
	}
	
	function __validates()
	{
		if (empty($this->validate))
			return true;
		
		$errors = array();
		
		// self
		foreach($this->data[$this->name] as $field => $value)
		{
			if (empty($this->validate[$field]))
				continue;
			foreach($this->validate[$field] as $validator => $msgstr)
			{
				if (method_exists($this, $validator))
				{
					if (call_user_func_array(array(&$this, $validator), array($field, $value)))
					{
						$errors[$field] = $msgstr;
						break 1;
					}
				}
				elseif (!preg_match($validator, $value))
				{
					$errors[$field] = $msgstr;
					break 1;
				}
			}
		}
		if (!empty($errors))
			$this->validationErrors[$this->name] = $errors;
		
		return empty($errors);
	}
	
	// inserts & updates
	
	function save($data=null, $validate=true, $type=null)
	{
		if (!is_null($data))
			$this->__setData(&$data);
		
		// validation
		if ($validate and !$this->validates())
			return false;
		
		// self
		if (!$this->__save($type))
			return false;
		
		// related
		foreach ($this->__getRelated() as $model)
		{
			if (isset($this->data[$model]) and !$this->$model->__save())
				return false;
		}
		
		return true;
	}
	
	function saveFields($data, $validate=false)
	{
		if ($validate and !$this->validates())
			return false;
		
		// conditions
		if (empty($this->_conditions))
		{
			if (is_null($this->id))
				return false;
			$this->conditions(array($this->primaryKey => $this->id));
		}
		
		// data
		$fields = array();
		foreach ($data as $field => $value)
		{
			array_push($fields,
				$this->db->field($field).' = '.$this->db->value($value));
		}
		$fields = implode(', ', $fields);
		
		// save
		$conditions = $this->db->conditions($this->_conditions, $this->_operator);
		$rs = $this->db->execute("UPDATE {$this->table} SET $fields $conditions ;");
		
		// reset
		$this->__reset();
		return $rs;
	}
	
	function saveField($field, $value, $validate=false)
	{
		return $this->saveFields(array($field => $value), $validate);
	}
	
	function saveMany($fields, &$values)
	{
		if (empty($values))
			return true;
		$fields = $this->db->fields($fields);
		
		// values
		foreach (array_keys($values) as $k)
		{
			foreach(array_keys($values[$k]) as $n) {
				$values[$k][$n] = $this->db->value($values[$k][$n]);
			}
			$values[$k] = '( '.implode(', ', $values[$k]).' ) ';
		}
		$values = implode(', ', $values);
		
		// save
		return $this->db->execute("INSERT INTO {$this->table} ( $fields ) VALUES $values ;");
	}
	
	function __save($type=null)
	{
		if (!$this->beforeSave())
			return false;
		
		$data =& $this->data[$this->name];
		
		// id
		if (isset($data[$this->primaryKey]))
			$this->id = $data[$this->primaryKey];
		
		if (!empty($this->id) and $type != 'insert')
		{
			// update
			if (array_key_exists('modified', $this->fields) and empty($data['modified']))
				$data['modified'] = date('Y-m-d H:i:s');
			
			$updates = array();
			
			foreach($data as $f => $v)
			{
				if (array_key_exists($f, $this->fields))
					array_push($updates, $this->db->field($f)." = ".$this->db->value($v));
			}
			$sql = "UPDATE {$this->table} SET ".implode(', ', $updates).
				" WHERE ".$this->db->field($this->primaryKey)." = ".$this->db->value($this->id);
			
			if (!$this->db->execute($sql))
				return false;
		}
		else
		{
			// insert
			if (array_key_exists('created', $this->fields) and empty($data['created']))
				$data['created'] = date('Y-m-d H:i:s');
			
			$fields = array();
			$values = array();
			
			foreach($data as $f => $v)
			{
				if (array_key_exists($f, $this->fields))
				{
					array_push($fields, $this->db->field($f));
					array_push($values, $this->db->value($v));
				}
			}
			$fields = implode(', ', $fields);
			$values = implode(', ', $values);
			$sql = "INSERT INTO {$this->table} ( $fields ) VALUES ( $values ) ;";
			
			if (!$this->db->execute($sql))
				return false;
			
			// new id
			if (empty($this->id))
				$this->id = $this->db->getLastInsertID();
			if (!isset($this->data[$this->name][$this->primaryKey]))
			 	$this->data[$this->name][$this->primaryKey] = $this->id;
		}
		
		return $this->afterSave();
	}
	
	
	// deletion
	
	function delete($id=null)
	{
		// conditions?
		$conditions = empty($this->_conditions) ? array('id' => empty($id) ? $this->id : $id) : $this->_conditions;
		$operator = $this->_operator;
		
		if (!$this->beforeDelete())
			return false;
		
		// associated data
		if ($this->recursive > 0)
		{
			// impacted entries
			$this->conditions(&$conditions);
			$ids = $this->listField('id', -1);
			
			if (!empty($ids))
			{
				// deletes impacted
				foreach ($this->__getRelated() as $model)
				{
					$this->$model->conditions(array($this->foreignKey => &$ids));
					$this->$model->operator('OR');
					$this->$model->deletionIsRecursive = true;
					
					if (!$this->$model->delete())
						return false;
				}
			}
		}
		
		// self
		if (!$this->__delete(&$conditions, $operator))
			return false;
		
		return $this->afterDelete();
	}
	
	function __delete($conditions=null, $operator=null)
	{
		if (!empty($conditions))
			$this->conditions(&$conditions);
		$conditions = $this->db->conditions($this->_conditions, isset($operator) ? $operator : $this->_operator);
		
		$rs = $this->db->execute("DELETE FROM {$this->table} $conditions ;");
#		debug("DELETE FROM {$this->table} $conditions ;");
#		$rs = true;
		
		if ($this->reset)
			$this->__reset();
		
		return $rs;
	}
	
	
	// some internals
	
	function & __getRelated()
	{
		if (!isset($this->__related))
		{
			if (!empty($this->hasMany) or !empty($this->hasOne))
			{
				$this->__related = array_unique(array_merge($this->hasMany, $this->hasOne));
				$this->__related = array_diff($this->__related, $this->belongsTo);
			}
			else
				$this->__related = array();
		}
		return $this->__related;
	}
	
	function __setData($data)
	{
		if (!isset($data[$this->name]))
			$this->data[$this->name] =& $data;
		else
			$this->data =& $data;
	}
	
	
	// callbacks
	
	function setBoolean($field)
	{
		$this->data[$this->name][$field] = isset($this->data[$this->name][$field]) ? true : false;
	}
	
	function beforeValidate() { return true; }
	function afterValidate()  { return true; }
	
	function beforeSave()     { return true; }
	function afterSave()      { return true; }
	
	function beforeDelete()   { return true; }
	function afterDelete()    { return true; }
}
?>